#include <iostream>
#include <pthread.h>
#include <queue>

// 这一个版本就是基于前面实现过的封装来进行操作(除了线程创建操作等...)
#include "Mutex.hpp"
#include "Cond.hpp"
using namespace myCond;
using namespace myMutex;


// 这个版本的生产者消费者模型的BlockQueue是直接使用pthread原生线程库来进行实现的


// 直接使用非类型模板参数定义队列大小 为了方便测试，就提供5个
template<class T, size_t N = 5>
class BlockQueue{
private:

bool IsFull(){
    return _block_queue.size() >= _capacity;
}

bool IsEmpty(){
    return _block_queue.empty();
}

// 还有一些细节：
//1. 在条件变量下等待是需要锁的，而且应该在临界区内进行等待！
//2. 首先，等待的时候，会自动释放锁；如果唤醒，就需要重新申请锁，只有申请到锁才能往下执行！
//3. 如果唤醒后一段时间内申请不到锁也是不怕的，因为会在申请锁那里进行等待！
//4. 上述逻辑在cond的相关接口内维护！

// 在指定条件变量下等待，应当在临界区内！
// 因为条件变量需要判定临界资源的情况！ 如果不在临界区判断，也会导致数据不一致！



public:
    BlockQueue()
        :_capacity(N),
        _consumer_waitNum(0),
        _productor_waitNum(0),
        _full(_lock),
        _empty(_lock)
    {}

    // 析构就什么也都不用写了
    ~BlockQueue(){}

    //使用RAII的思想来进行加锁解锁

    void Push(T data){
        // 是生产者将生产的东西入队

        Lock_Guard guard(_lock);
        while(IsFull()){
            // 如果队列此时处于满的状态，生产者就不能再生产东西入队！
            // 所以就要在满的条件变量下进行等待
            
            ++_productor_waitNum;
            // 但是如果使用if(IsFull())会出问题：
            // 1. 如果pthread_cond_wait如果说等待失败，也就是这个函数执行不成功，返回非0值
            //那么此时就不会再执行该函数了，就往下执行。 但是，此时在临界区内(加了锁)，只有当前线程能进来访问
            //此时队列仍然为满！如果使用if判断，那么就直接往下执行了。那么队列为满，还要再往里面push，这就错了

            // 2. 还有一种情况就是：
            //如果当前消费者就消费了一个，但是消费者那边使用pthread_cond_broadcast把全部生产者唤醒
            //很可能会出现，连着几个生产者持有锁
            //但是，消费者那边只消费了一个，但一旦出现上述情况
            //就会导致，后序的消费者无法消费，但是生产者拿到锁就往下执行了
            //此时队列仍然是满的！后序线程往里面push，也是错的！
            
            // 所以，这就不能使用if来进行判断，而必须使用while(IsFull())来进行判断！

            _full.Wait();
            --_productor_waitNum;            
        }

        // 此时说明可以生产东西放入队列
        _block_queue.push(data);

        // 而且此时，一定是有产品给消费者进行消费的 -> 唤醒消费者 -> 前提是，有消费者正在处于等待
        if(_consumer_waitNum > 0){
            _empty.Signal();
            std::cout << "productor : 唤醒消费者" << std::endl;
        }


        // 单消费单生产的话 -> 每生产一个就通知一次！
        // 这里在解锁前和解锁后唤醒都无所谓！ 因为如果唤醒后：
        // 在pthread_cond_wait下，是需要传入一把锁
        // 即如果使用pthread_cond_wait，会自动释放这个锁给别人用。唤醒后，需要重新申请锁，才会继续往下走
        // 如果唤醒后一段时间内没申请到锁，无所谓，会在申请锁那里阻塞等待！
        // 所以，这就导致写在解锁前后都无所谓！！！！！
    }

    T Pop(){
        // 这个是消费者拿东西出队消费

        Lock_Guard guard(_lock);

        // 这里用while和Push接口类似！
        while(IsEmpty()){
            // 如果队列此时处于空的状态，消费就不能从队中拿东西消费！
            // 所以就要在空的条件变量下进行等待
            ++_consumer_waitNum;
            _empty.Wait();
            --_consumer_waitNum;
        }

        // 此时说明，队列不为空，可以消费
        T product = _block_queue.front();
        _block_queue.pop();
        
        // 此时就可以进行唤醒生产者 -> 前提是 -> 当前有生产者在等待
        if(_productor_waitNum > 0){
            _full.Signal();
            std::cout << "consumer : 唤醒生产者" << std::endl;
        }

        return product;
    }

    // 这个接口建议不要使用，只是为了方便在单消费，单生产的情况下输出日志用的
    T Front(){
        return _block_queue.front();
    }


private:
    std::queue<T> _block_queue;
    size_t _capacity;  // 当前阻塞队列的容量
    Mutex _lock; // 因为这里封装的Cond是要传入锁的，所以需要保证Mutex在Cond之前定义
    Cond _full;  // 判断是否慢了的条件变量，生产者在队列满的情况下不能push
    Cond _empty; // 判断是否为空的条件变量，消费者在队列空的情况下不能pop

    size_t _consumer_waitNum;  // 消费者在等待的个数
    size_t _productor_waitNum; // 生产者在等待的个数
};

